package ServiceProvider;

import java.awt.image.BufferedImage;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Random;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.imageio.ImageIO;

import CertificateAuthority.CA;
import CertificateAuthority.Cert;

public class SP implements Runnable {
	public String[] listOfCerts;
	public int selectedCertID;
	private Socket server;
	private BufferedWriter writer;
	private SecretKeySpec symetricKeyFromCard;
	private  byte[] challengeBytes;
	private Method logMethod;
	private SPGUI guiInstance;
	
	
	private static final byte SP_CONNECT = 0x01;
	private static final byte SP_AUTH_SP = 0x02;
	private static final byte SP_AUTH_CARD = 0x03;
	private static final byte SP_ATTR = 0x04;
	private static final byte SP_CHALLENGE_PLUS_ONE = 0x05;
	private static final byte SP_BREAK = 0x06;
	
	private static final byte SP_OK = 0x10;
	private static final byte SP_NK = 0x11;
	
	public SP(Method logMethod, SPGUI guiInstance) {
		System.out.println("been here" );
		this.listOfCerts = loadCerts("secure_files/SP/");
		
		this.selectedCertID = 0;
		this.logMethod = logMethod;
		this.guiInstance = guiInstance;
		System.out.println("been here" + guiInstance.getName());
	}
		
	public void log(String s) {
		try {
			logMethod.invoke(this.guiInstance, s + "\n");
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
	}
	
	public void run() {
		log("-Connect to middleware");
		/* Config*/
		try {
	     InetAddress serveraddress;
	     serveraddress = InetAddress.getByName("127.0.0.1");
		
	     server = new Socket(serveraddress,2222);
	     log("-- Succesfully connected to Middleware \n");
	     DataOutputStream dos = new DataOutputStream(server.getOutputStream());
	     DataInputStream dis = new DataInputStream(server.getInputStream());
	     log("-Sending SP_CONNECT");
	     dos.writeByte(SP_CONNECT);
	     dos.flush();
	     
	     
	     boolean connect_succes = dis.readBoolean();
	     if(connect_succes) {
	    	 log("==> Succesfully connected to the card \n");
	     }
	     else{
	    	 log("==> Could not connect to the card. \n");
	    	 throw new Exception ("Failed to connect to card");
	     }
	     
	     /***** Inlezen certificaat *****/
	     String name = listOfCerts[selectedCertID];
		 byte[] certificateF = readFile("secure_files/SP/" + name);
		 log("-Sending Authenticate SP Command");
		 dos.writeByte(SP_AUTH_SP);
		 dos.writeShort(certificateF.length);
		 dos.write(certificateF);
		 
		 /***** Inlezen challenge + symmetric key from card *****/
		 short lengthInput = dis.readShort();
		 byte[] response = new byte[lengthInput];
		 dis.read(response, 0, lengthInput);
	     byte[] challengePlusOne = authenticateServiceProvider(response);
	     log("- Sending Challenge +1 Command");
	     dos.writeByte(SP_CHALLENGE_PLUS_ONE);
	     dos.writeShort(challengePlusOne.length);
	     dos.write(challengePlusOne);
	     
	     boolean challengeAccepted = dis.readBoolean();
	     if(challengeAccepted) {
	    	 log("==> Challenge accepted by the card \n");
	     }
	     else{
	    	 log("==> Challenge NOT accepted by card \n");
	     }
	     
	     byte[] encryptedChallenge = GenerateEncryptedCardChallenge();
	     log("-Sending Autheticate card command");
	     dos.writeByte(SP_AUTH_CARD);
	     dos.writeShort(encryptedChallenge.length);
	     dos.write(encryptedChallenge);
	     short length = dis.readShort();
	     response = new byte[length];
	     dis.read(response, 0, length);
	     boolean cardVerified = AuthenticateCard(response);
	     if(cardVerified) {
	    	log("==> Authenticate Card succeeded \n"); 
	     } else {
	    	log("==> Authenticate Card NOT succeeded \n"); 
	     }
	     
	     dos.writeByte(SP_ATTR);
	     log("-Sending Get attributes command");
	     short lengthAttributes = dis.readShort();
	     byte[] ecnryptedAttributes = new byte[lengthAttributes];
	     dis.read(ecnryptedAttributes, 0, lengthAttributes);
	     processAttributes(ecnryptedAttributes);
	     
	     /****  Connecties sluiten ****/
	     dos.writeByte(SP_BREAK);
	     server.close();
	     dis.close();
	     dos.close();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				server.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	public void processAttributes(byte[] attributes_encrypted ) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
		byte[] ivdata = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
		IvParameterSpec spec = new IvParameterSpec(ivdata);
		
		log("-- Decrypt attributes witj Symetric key from card");
		Cipher cipherAes = Cipher.getInstance("AES/CBC/NoPadding");
		cipherAes.init(Cipher.DECRYPT_MODE, symetricKeyFromCard, spec);
		byte[] attributes = cipherAes.doFinal(attributes_encrypted);
		
		int offset = 0;
		do{
			String value = null;
			byte[] img = null;
			short length = -1;
			byte type = attributes[offset];
			if(type == 6) {	//Als foto
				 length =(short)( ((attributes[1 + offset]&0xFF)<<8) | (attributes[2 + offset]&0xFF) );
				 img = new byte[length];
				 System.arraycopy(attributes, offset+3, img, 0, length);
			}
			else{
				length = attributes[offset+1];
				if(type==100) break;	//100 betekent einde van attributen! Moet dus altijd meegestuurd worden als laatste
				value = new String(attributes, offset+2, length);
			}
			switch(type) {
				case 0: log("---- Naam = " + value);
						break;
				case 1: log("---- Adres = " + value);
						break;
				case 2: log("---- Land = " + value);
						break;
				case 3: log("---- Geboortedatum = " + value);
						break;
				case 4: log("---- Leeftijd = " + value);
						break;
				case 5: log("---- Geslacht = " + value);
						break;
				case 6: ByteArrayInputStream bos = new ByteArrayInputStream(img);
						BufferedImage bufferedImage = ImageIO.read(bos);
						log("---- Foto: " + bufferedImage.getHeight());
						break;
				default: break;
			}
			offset += length + 2;
		}while(offset < attributes.length);
		byte[] lengte = new byte[2];
		System.arraycopy(attributes, offset+3, lengte, 0, 2);
		short lengteS = (short)( ((lengte[0]&0xFF)<<8) | (lengte[1]&0xFF) );
		byte[] nymUSP = new byte[lengteS];
		System.arraycopy(attributes, offset+5, nymUSP, 0, nymUSP.length);
		log("-- nymphOK: " + nymUSP.length);
	}
	
	 public byte[] GenerateEncryptedCardChallenge() throws Exception {
		 //SP: Generate Challenge
		 log("-- Generating and encrypting challenge with Symetric key from Card");
		 Random rand = new Random();
		 short challenge =(short) rand.nextInt(Short.MAX_VALUE);
		 log("---- Challenge= " + challenge);
		 ByteArrayOutputStream bos = new ByteArrayOutputStream();  
	     DataOutputStream dos = new DataOutputStream(bos);  
	     dos.writeShort(challenge);  
	     challengeBytes = new byte[16];
	     System.arraycopy( bos.toByteArray(), 0, challengeBytes, 0, 2);	//Eerste 2 bytes zijn data rest is "padding"
		 
		 //SP: Encrypteren met Symetrische sleutel		 
		 byte[] ivdata = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
		 IvParameterSpec spec = new IvParameterSpec(ivdata);
		 Cipher cipherAes = Cipher.getInstance("AES/CBC/NoPadding");
		 cipherAes.init(Cipher.ENCRYPT_MODE, symetricKeyFromCard, spec);
		 byte[] encryptedChallenge = cipherAes.doFinal(challengeBytes);
		 
		 return encryptedChallenge;
	 }
	 
	 public boolean AuthenticateCard(byte[] input) throws Exception {	 
		 //SP: Symetrisch decrypteren
		 log("-- Decrypt with symetric key from card and check signature");
		 byte[] ivdata = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
		 IvParameterSpec spec = new IvParameterSpec(ivdata);
		 
		 Cipher cipherAes = Cipher.getInstance("AES/CBC/NoPadding");
		 cipherAes.init(Cipher.DECRYPT_MODE, symetricKeyFromCard, spec);
		 byte[] decryptedBytes = cipherAes.doFinal(input);
		 
		//SP: Common certificaat controleren
		 log("-- Decrypt with symetric key from card and check Common Certificate");
		 int lengthSignature = 64;
		 byte[] signature = new byte[lengthSignature];
		 byte[] cert = new byte[decryptedBytes.length-(lengthSignature)];
		 System.arraycopy(decryptedBytes, 0, signature, 0, signature.length);
		 System.arraycopy(decryptedBytes, signature.length, cert, 0, cert.length);
		 Cert common_cert = Cert.FromByteArray(cert);
		 
		 //SP:Common certificaat testen
		 byte[] ca_bytes = CA.readFile("secure_files/certificate_ca.cert");
		 Cert ca_cert = Cert.FromByteArray(ca_bytes);
		 
		 boolean verify = common_cert.Verify(ca_cert.publicKey);
		 
		 if(verify == false) {
			 log("---- Common certificate is not ok, don't trust this card!");
			 throw new Exception("Identification failed from common certificate");
		 } else {
			 log("---- Common certificate ok!");
		 }
		 
		 //SP:Signature controleren van challenge met common
		 log("-- Decrypt with common public key and check Challenge");
		 byte[] result = new byte[16];
		 System.arraycopy(challengeBytes, 0, result, 0, 2);
		 Signature rsa = Signature.getInstance("SHA1withRSA");
		 rsa.initVerify(common_cert.publicKey);
	     rsa.update(result);
		 boolean verify2 =  rsa.verify(signature);
		
		 if(verify2 == false) {
			 log("---- Challenge is not ok, don't trust this card!");
			 throw new Exception("Challenge is not ok, don't trust this card");
		 } else {
			 log("---- Signature is ok!");
			 return true;
		 }


	 }
	
	 public byte[] authenticateServiceProvider(byte[] input) throws Exception { 		 
		//Challenge ontvangen opdelen in key en challenge
		 log("-- Decrypt symetric key from card with private key from SP certificate");
		 byte[] symkeyDataEncrypted = new byte[64];
		 byte[] challengeEncrypted= new byte[input.length-symkeyDataEncrypted.length];
		 System.arraycopy(input, 0, symkeyDataEncrypted, 0, symkeyDataEncrypted.length); 	 //arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 
		 System.arraycopy(input, symkeyDataEncrypted.length, challengeEncrypted, 0, input.length-symkeyDataEncrypted.length);
		 
		 String[] name = listOfCerts[selectedCertID].split(".cert");
		 String[] name2 = name[0].split("_");
		 byte[] priv_eGov1 = readFile("secure_files/SP/private_"+ name2[1] +".key");
		 /* Get the private key object */
		 EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(priv_eGov1);
         KeyFactory generator = KeyFactory.getInstance("RSA");
         PrivateKey privateKey = generator.generatePrivate(privateKeySpec);
		 sun.security.rsa.RSAPrivateCrtKeyImpl pk = (sun.security.rsa.RSAPrivateCrtKeyImpl) privateKey;
			
		 //Symetrische key decrypteren
		 Cipher cipher = Cipher.getInstance("RSA");
		 cipher.init(Cipher.DECRYPT_MODE, pk);
		 cipher.update(symkeyDataEncrypted);
		 byte[] symKeyData = cipher.doFinal();
		 
		 byte[] ivdata = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
		 IvParameterSpec spec = new IvParameterSpec(ivdata);
		 
		 //Symetrische key aanmaken en challenge decrypteren
		 log("-- Decrypt Challenge with Symetric key from card");
		 symetricKeyFromCard = new SecretKeySpec(symKeyData, "AES");
		 
		 Cipher cipherAes = Cipher.getInstance("AES/CBC/NoPadding");
		 cipherAes.init(Cipher.DECRYPT_MODE, symetricKeyFromCard, spec);
		 byte[] decryptedBytes = cipherAes.doFinal(challengeEncrypted);
		 
		 //Challenge en subject eruit halen
		 byte[] challengeData = new byte[2];
		 byte[] subject = new byte[challengeEncrypted.length-challengeData.length];
		 System.arraycopy(decryptedBytes, 0, challengeData, 0, challengeData.length); 	 //arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 
		 System.arraycopy(decryptedBytes, challengeData.length, subject, 0, decryptedBytes.length-challengeData.length);
		 
		 log("---- Subject is: " + new String(subject));
		 
		 //Challenge met 1 verhogen
		 log("-- Incremnet Challenge and encrypt with Symetric key from card");
		 ByteBuffer bbuf = ByteBuffer.wrap(challengeData);
		 short challenge = bbuf.getShort();
		 log("---- Challenge = " + challenge);
		 challenge++;
		 log("---- Challenge+1 = " + challenge);
		 
		 //Encrypteren
		 ByteArrayOutputStream bos = new ByteArrayOutputStream();  
		 DataOutputStream dos = new DataOutputStream(bos);  
	     dos.writeShort(challenge);  
	     byte[] data = bos.toByteArray();   
		 byte[] alignedData = new byte[16];
		 System.arraycopy(data, 0, alignedData, 0, data.length);
			 
		 cipherAes.init(Cipher.ENCRYPT_MODE, symetricKeyFromCard, spec);	//BELANGRIJK!!!! spec moet meegegeven worden!
		 byte[] encryptedBytes = cipherAes.doFinal(alignedData);
		 
		return encryptedBytes;
	 }
	
	public String getCertInfo(String name) {
		byte[] cert_bytes = readFile("secure_files/SP/" + name);
		StringBuilder sb = new StringBuilder();
		try {
			Cert cert = Cert.FromByteArray(cert_bytes);
			sb.append("########################Certificate########################\n");
			sb.append("Chosen domain: '" + cert.GetDomain() + "'\n" + "Subject: '" + cert.Getsubject()+ "'\n");
			sb.append("########################################################\n\n");
		} catch (InvalidKeySpecException e) {
			e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		
		return sb.toString();
	}
	
	public String[] loadCerts(String path) {
		  File folder = new File(path);
		  File[] listOfFiles = folder.listFiles();
		  int teller = 0;
		  for(int f = 0; f < listOfFiles.length; f++) {
			  File file = listOfFiles[f];
			  String filename = file.getName();
			  if(filename.endsWith("cert")) {
				  teller++;
				  System.out.print(f+ ": ");
			  }
		  }
		  String[] listOfCerts = new String[teller];
		  teller =0;
		  for(int f = 0; f < listOfFiles.length; f++) {
			  File file = listOfFiles[f];
			  String filename = file.getName();
			  if(filename.endsWith("cert")) {
				  listOfCerts[teller] = filename;
				  teller++;
			  }
		  }
		  return listOfCerts;
	}
	
	public static byte[] readFile(String filename) {
		File file = new File(filename);
		byte[] fileContent = null;
		try {
			FileInputStream myCert = new FileInputStream(filename);
			fileContent = new byte[(int)file.length()];
			myCert.read(fileContent);
		} catch (Exception e) {

			e.printStackTrace();
		}
		return fileContent;
	}
	
	
}
